#!/usr/bin/python2.4
# jhebert@cs.washington.edu

import sys
import Queue
import threading
import traceback

class ProgramNode:
  """This class represents a base program node.
  It must be both runnable and able to determine if
  it is ready to run. """

  def __init__(self, cr, name, log):
    self.statusLog=log
    self.codeObj = ''
    # The chartRunner managing this node.
    self.runners = [cr]
    # The variables that this node is listening for.
    self.borderInputs = {}
    self.stateVars = {}
    # A mapping from functions in the program to the variables they need.
    self.functionMaps = {}
    # The queue of functions that are ready to run.
    self.runnableQueue = Queue.Queue()
    self.runningArgList = {}
    self.nodeName = name
    self.listInputs = {}
    self.runnerHelper = RunnerHelper(self)
    self.mutex = threading.RLock()

  ## <Called from chartParser> ##

  def AddRunner(self, runner):
    """ Adding another chartRunner to this node. """
    message="Adding runner for node."
    self.statusLog.put((self.nodeName, message, runner.nodeName))
    self.mutex.acquire()
    self.runners.append(runner)
    self.mutex.release()
    self.UpdateInfo()

  def SetCodeObj(self, p):
    """ Set the code object to run. """
    self.statusLog.put((self.nodeName, "Setting code object",p))
    self.codeObj = p

  def AddListenerVariable(self, var):
    """ Add a listener variable to this node. """
    self.statusLog.put((self.nodeName,"Adding listener var",var))
    i = var.find('list ')
    if(i>-1):
      toAdd = var[i+5:]
      self.mutex.acquire()
      self.borderInputs[toAdd] = []
      self.listInputs[toAdd] = None
      self.mutex.release()
      return toAdd
    else:
      self.mutex.acquire()
      self.borderInputs[var] = []
      self.mutex.release()
    self.UpdateInfo()
    return var

  def AddStateVariable(self, var):
    """ Add a state variable to this program node. """
    self.statusLog.put((self.nodeName,"Adding state var",var))
    shouldEmit = True
    try:
      varName, initValue = [a.strip() for a in var.split('=')]
      value = eval(initValue)
    except ValueError:
      varName, shouldEmit=a.strip(), False
    name = self.AddListenerVariable(varName)
    self.mutex.acquire()
    self.stateVars[name] = None
    self.mutex.release()
    if(shouldEmit):
      self.Emit(self.nodeName, name, value)
    self.UpdateInfo()
    return name

  def SetFunctionMaps(self, maps):
    """ Set the dictionary of function to variable mappings. """
    self.statusLog.put((self.nodeName,"Setting function maps",maps))
    self.functionMaps = maps

  ## <End called from chartParser> ##
  ## <Called from chartRunner> ##

  def IsListeningFor(self, varName):
    """ Is this programNode listening for varName? """
    toReturn = (varName in self.borderInputs)
    message = 'Am I listening for: ' + varName
    self.statusLog.put((self.nodeName,message,str(toReturn)))
    return toReturn

  def Emit(self, caller, varName, varValue):
    """ Add a variable to dataStruct holding waiting values. """
    toShow=''.join([str(varName),' = ',str(varValue)])
    self.statusLog.put((self.nodeName,"Received variable",toShow))
    self.mutex.acquire()
    self.borderInputs[varName].append(varValue)
    self.mutex.release()

  def Run(self):
    """ If ready to run grab the appropriate variables and
    prep the environment to run in. """
    try:
      argList = self.runnableQueue.get(False)
    except Queue.Empty:
      return
    toShow = str(argList)
    self.statusLog.put((self.nodeName,"Allowed to run",toShow))
    self.__MarkStart(argList)
    env = {}
    env['___chartRunner___'] = self.runnerHelper
    env['___nodeName___'] = self.nodeName
    for valPair in argList:
      name, val = valPair
      env[name] = val
    try:
      eval(self.codeObj, env)
    except Exception, err:
      message = 'Error running node:'+self.nodeName+":"+str(err)
      print sys.exc_info()[0]
      print sys.exc_info()[1]
      #print dir(sys.exc_info()[2])
      #print dir((sys.exc_info()[2]).tb_frame)
      traceback.print_tb(sys.exc_info()[2])

      
      self.statusLog.put((self.nodeName,'_ERROR_', message))
    self.__MarkFinish(argList)
    self.__SaveStateVars(env)

  def __MarkStart(self, argList):
    self.mutex.acquire()
    toAdd = str(argList)
    if(toAdd in self.runningArgList):
      self.runningArgList[toAdd] += 1
    else:
      self.runningArgList[toAdd] = 1
    self.mutex.release()

  def __MarkFinish(self, argList):
    self.mutex.acquire()
    item=str(argList)
    if(self.runningArgList[item]==1):
      del self.runningArgList[item]
    else:
      self.runningArgList[item] -= 1
    self.mutex.release()

  def ReadyToRun(self):
    """ Determines if the set vars contains enough variables and
    appends to runnable. Return true if some variables are ready to run. """
    self.mutex.acquire()
    #TODO: check this.
    for func in self.functionMaps:
      vars = self.functionMaps[func]
      temp = [1 for var in vars if len(self.borderInputs[var])>0]
      if(sum(temp)==len(vars)):
        argList=[]
        for var in vars:
          nameToAdd=var
          if(var in self.listInputs):
            valueToAdd=self.borderInputs[var]
            self.borderInputs[var] = []
          else:
            valueToAdd=self.borderInputs[var][0]
            self.borderInputs[var] = self.borderInputs[var][1:]
          argList.append((nameToAdd, valueToAdd))
        toAdd=str(argList)
        if(len(argList)>0):
          self.statusLog.put((self.nodeName,"Adding to run queue",toAdd))
          self.runnableQueue.put(argList)
    self.mutex.release()
    toReturn = not(self.runnableQueue.empty())
    self.statusLog.put((self.nodeName,"Ready to run",str(toReturn)))
    return toReturn

    ## <End of called from chartParser> ##

  def __SaveStateVars(self, env):
    for var in self.stateVars:
      if(var in env):
        self.Emit(self.nodeName, var, env[var])

  def AssembleInfoPage(self):
    toReturn=(
      '<b>Name:</b> %(name)s\n'
      '<b>Inputs:</b> %(inputs)s\n'
      '<b>ChartRunners:</b> %(runners)s\n'
      '<b>Function maps:</b> %(funcs)s\n') % ({
      'name' : self.nodeName,
      'inputs' : ', '.join(self.borderInputs.keys()),
      'runners':', '.join([a.nodeName for a in self.runners]),
      'funcs' : self.FormatHash(self.functionMaps), })
    return toReturn

  def FormatHash(self, hashTable):
    toReturn=[]
    for item in hashTable:
      toReturn.append(str(item)+' : '+str(hashTable[item]))
    return '\n'.join(toReturn)

  def UpdateInfo(self):
    data=self.AssembleInfoPage()
    self.statusLog.put((self.nodeName,'info',data))

class RunnerHelper:

  def __init__(self, programNode):
    self.programNode = programNode

  def Emit(self, caller, varName, varValue):
    for runner in self.programNode.runners:
      runner.Emit(caller, varName, varValue)
